/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.util;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Class that takes a combined output destination and provides two output
 * writers, one of which ends up writing to the left column and one which goes
 * on the right.
 */
public final class TwoColumnOutput {

	/** {@code non-null;} underlying writer for final output */
	private final Writer out;

	/** {@code > 0;} the left column width */
	private final int leftWidth;

	/** {@code non-null;} pending left column output */
	private final StringBuffer leftBuf;

	/** {@code non-null;} pending right column output */
	private final StringBuffer rightBuf;

	/** {@code non-null;} left column writer */
	private final IndentingWriter leftColumn;

	/** {@code non-null;} right column writer */
	private final IndentingWriter rightColumn;

	/**
	 * Turns the given two strings (with widths) and spacer into a formatted
	 * two-column string.
	 * 
	 * @param s1
	 *            {@code non-null;} first string
	 * @param width1
	 *            {@code > 0;} width of the first column
	 * @param spacer
	 *            {@code non-null;} spacer string
	 * @param s2
	 *            {@code non-null;} second string
	 * @param width2
	 *            {@code > 0;} width of the second column
	 * @return {@code non-null;} an appropriately-formatted string
	 */
	public static String toString(String s1, int width1, String spacer,
			String s2, int width2) {
		int len1 = s1.length();
		int len2 = s2.length();

		StringWriter sw = new StringWriter((len1 + len2) * 3);
		TwoColumnOutput twoOut = new TwoColumnOutput(sw, width1, width2, spacer);

		try {
			twoOut.getLeft().write(s1);
			twoOut.getRight().write(s2);
		} catch (IOException ex) {
			throw new RuntimeException("shouldn't happen", ex);
		}

		twoOut.flush();
		return sw.toString();
	}

	/**
	 * Constructs an instance.
	 * 
	 * @param out
	 *            {@code non-null;} writer to send final output to
	 * @param leftWidth
	 *            {@code > 0;} width of the left column, in characters
	 * @param rightWidth
	 *            {@code > 0;} width of the right column, in characters
	 * @param spacer
	 *            {@code non-null;} spacer string to sit between the two columns
	 */
	public TwoColumnOutput(Writer out, int leftWidth, int rightWidth,
			String spacer) {
		if (out == null) {
			throw new NullPointerException("out == null");
		}

		if (leftWidth < 1) {
			throw new IllegalArgumentException("leftWidth < 1");
		}

		if (rightWidth < 1) {
			throw new IllegalArgumentException("rightWidth < 1");
		}

		if (spacer == null) {
			throw new NullPointerException("spacer == null");
		}

		StringWriter leftWriter = new StringWriter(1000);
		StringWriter rightWriter = new StringWriter(1000);

		this.out = out;
		this.leftWidth = leftWidth;
		this.leftBuf = leftWriter.getBuffer();
		this.rightBuf = rightWriter.getBuffer();
		this.leftColumn = new IndentingWriter(leftWriter, leftWidth);
		this.rightColumn = new IndentingWriter(rightWriter, rightWidth, spacer);
	}

	/**
	 * Constructs an instance.
	 * 
	 * @param out
	 *            {@code non-null;} stream to send final output to
	 * @param leftWidth
	 *            {@code >= 1;} width of the left column, in characters
	 * @param rightWidth
	 *            {@code >= 1;} width of the right column, in characters
	 * @param spacer
	 *            {@code non-null;} spacer string to sit between the two columns
	 */
	public TwoColumnOutput(OutputStream out, int leftWidth, int rightWidth,
			String spacer) {
		this(new OutputStreamWriter(out), leftWidth, rightWidth, spacer);
	}

	/**
	 * Gets the writer to use to write to the left column.
	 * 
	 * @return {@code non-null;} the left column writer
	 */
	public Writer getLeft() {
		return leftColumn;
	}

	/**
	 * Gets the writer to use to write to the right column.
	 * 
	 * @return {@code non-null;} the right column writer
	 */
	public Writer getRight() {
		return rightColumn;
	}

	/**
	 * Flushes the output. If there are more lines of pending output in one
	 * column, then the other column will get filled with blank lines.
	 */
	public void flush() {
		try {
			appendNewlineIfNecessary(leftBuf, leftColumn);
			appendNewlineIfNecessary(rightBuf, rightColumn);
			outputFullLines();
			flushLeft();
			flushRight();
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * Outputs to the final destination as many full line pairs as there are in
	 * the pending output, removing those lines from their respective buffers.
	 * This method terminates when at least one of the two column buffers is
	 * empty.
	 */
	private void outputFullLines() throws IOException {
		for (;;) {
			int leftLen = leftBuf.indexOf("\n");
			if (leftLen < 0) {
				return;
			}

			int rightLen = rightBuf.indexOf("\n");
			if (rightLen < 0) {
				return;
			}

			if (leftLen != 0) {
				out.write(leftBuf.substring(0, leftLen));
			}

			if (rightLen != 0) {
				writeSpaces(out, leftWidth - leftLen);
				out.write(rightBuf.substring(0, rightLen));
			}

			out.write('\n');

			leftBuf.delete(0, leftLen + 1);
			rightBuf.delete(0, rightLen + 1);
		}
	}

	/**
	 * Flushes the left column buffer, printing it and clearing the buffer. If
	 * the buffer is already empty, this does nothing.
	 */
	private void flushLeft() throws IOException {
		appendNewlineIfNecessary(leftBuf, leftColumn);

		while (leftBuf.length() != 0) {
			rightColumn.write('\n');
			outputFullLines();
		}
	}

	/**
	 * Flushes the right column buffer, printing it and clearing the buffer. If
	 * the buffer is already empty, this does nothing.
	 */
	private void flushRight() throws IOException {
		appendNewlineIfNecessary(rightBuf, rightColumn);

		while (rightBuf.length() != 0) {
			leftColumn.write('\n');
			outputFullLines();
		}
	}

	/**
	 * Appends a newline to the given buffer via the given writer, but only if
	 * it isn't empty and doesn't already end with one.
	 * 
	 * @param buf
	 *            {@code non-null;} the buffer in question
	 * @param out
	 *            {@code non-null;} the writer to use
	 */
	private static void appendNewlineIfNecessary(StringBuffer buf, Writer out)
			throws IOException {
		int len = buf.length();

		if ((len != 0) && (buf.charAt(len - 1) != '\n')) {
			out.write('\n');
		}
	}

	/**
	 * Writes the given number of spaces to the given writer.
	 * 
	 * @param out
	 *            {@code non-null;} where to write
	 * @param amt
	 *            {@code >= 0;} the number of spaces to write
	 */
	private static void writeSpaces(Writer out, int amt) throws IOException {
		while (amt > 0) {
			out.write(' ');
			amt--;
		}
	}
}
